package ch.areion.observator;

import ch.areion.common.message.*;
import ch.areion.observator.server.ClientConnection;
import ch.areion.observator.server.LogServer;
import ch.areion.observator.server.ServerListener;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.GridLayout;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.StringTokenizer;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSeparator;
import javax.swing.JSplitPane;
import javax.swing.JTextPane;
import javax.swing.KeyStroke;
import javax.swing.Timer;
import javax.swing.border.BevelBorder;
import javax.swing.text.BadLocationException;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;

/**
 * Main Gui class
 * @author leupibr
 */
public class AreionObservator extends JFrame implements ServerListener {
	/**
	 * 
	 */
	private static final long serialVersionUID = 242535698777268425L;
	private final static Color COLORWARN = new Color(255, 127, 0);
	private final static int FLUSHCYCLE = 200;

	/* Content */
	private JSplitPane spMain;
	private JSplitPane spRight;
	private JPanel pnlLeft;
	private JPanel pnlCore, pnlEngine, pnlSensor, pnlIO, pnlPeriphery, pnlComm;
	private HashMap<String, InfoItem> coreItems, engineItems, sensorItems,
	    ioItems, peripheryItems, commItems;

	private JPanel pnlStats, pnlLog;
	private JScrollPane spLog;
	private JTextPane pnLog;

	/* Status panel */
	private JLabel lblMsgCount, lblHostAddr;
	private JLabel lblClientAddr, lblTime;

	/* Menu */
	private JCheckBoxMenuItem mnuViewVersion;
	private JCheckBoxMenuItem mnuViewTimestamp;
	private JCheckBoxMenuItem mnuViewType;

	/* Messaging */
	private ArrayList<Message> msgBuffer;
	private StyledDocument doc;

	/* Styling */
	private Style def, def_b, def_i;
	private Style err, err_b, err_i;
	private Style warn, warn_b, warn_i;
	private Style inf, inf_b, inf_i;

	/* Communication */
	private LogServer logServer;

	/**
	 * Creates new form GUI.
	 */
	public AreionObservator() {
		initComponents();
		initSubComponents();
		initBuffer();

		initServer(4444);
	}

	/**
	 * Initializes the group box sub items lists.
	 */
	private void initSubComponents() {
		coreItems = new HashMap<String, InfoItem>();
		engineItems = new HashMap<String, InfoItem>();
		sensorItems = new HashMap<String, InfoItem>();
		ioItems = new HashMap<String, InfoItem>();
		peripheryItems = new HashMap<String, InfoItem>();
		commItems = new HashMap<String, InfoItem>();
	}

	/**
	 * Inits the message buffer for received buffers.
	 */
	private void initBuffer() {
		// init buffer
		msgBuffer = new ArrayList<Message>();
		ActionListener clrTask = new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent ae) {
				setTime();
				clearBuffer();
			}
		};
		Timer tmeClr = new Timer(FLUSHCYCLE, clrTask);
		tmeClr.start();
	}

	/**
	 * Sets the time on the gui.
	 */
	private void setTime() {
		lblTime.setText(Calendar.getInstance().getTime().toString());
	}

	/**
	 * Initializes the server.
	 * @param port The port to listen to
	 */
	private void initServer(final int port) {
		String addr = "";
		try {
			addr = InetAddress.getLocalHost().getHostAddress();
		}
		catch(UnknownHostException ex) {
			addMessage(new Message(Version.V01, ch.areion.common.message.Type.ERROR,
			    Module.COMMUNICATION, ex.getMessage()));
		}

		// create and start server
		logServer = new LogServer(port);
		new Thread(logServer).start();

		// consume messages
		logServer.register(this);

		// set host information
		lblHostAddr.setText("Host" + " = " + addr + ":" + port);
		lblHostAddr.setForeground(new Color(32, 128, 0));
	}

	/**
	 * Initializes the components.
	 */
	private void initComponents() {
		this.getContentPane().add(initMenuPanel(), BorderLayout.PAGE_START);
		this.getContentPane().add(initContentPanel(), BorderLayout.CENTER);
		this.getContentPane().add(initStatusPanel(), BorderLayout.PAGE_END);

		setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

		ComponentAdapter ad = new ComponentAdapter() {
			@Override
			public void componentResized(ComponentEvent ce) {
				spRight.setDividerLocation(.35);
				spMain.setDividerLocation(.5);
			}
		};
		this.addComponentListener(ad);

		this.pack();
		this.setVisible(true);
		this.setExtendedState(this.getExtendedState() | JFrame.MAXIMIZED_BOTH);
	}

	/**
	 * Initializes the menu.
	 * @return A menu instance
	 */
	private JMenuBar initMenuPanel() {
		JMenuBar mnu = new JMenuBar();

		JMenu mnuFile = new JMenu("File");
		JMenu mnuView = new JMenu("View");
		JMenu mnuViewLog = new JMenu("Log");
		JMenu mnuHelp = new JMenu("Help");

		/* File */
		JMenuItem mnuFileExit = new JMenuItem("Exit");
		mnuFileExit.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X,
		    InputEvent.CTRL_MASK));
		mnuFileExit.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent ae) {
				System.exit(0);
			}
		});
		/* View */
		mnuViewVersion = new JCheckBoxMenuItem("Version", true);
		mnuViewVersion.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent ae) {
				mnuViewVersion.setState(mnuViewVersion.getState());
			}
		});
		mnuViewTimestamp = new JCheckBoxMenuItem("Timestamp", true);
		mnuViewTimestamp.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent ae) {
				mnuViewTimestamp.setState(mnuViewTimestamp.getState());
			}
		});
		mnuViewType = new JCheckBoxMenuItem("Type", true);
		mnuViewType.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent ae) {
				mnuViewType.setState(mnuViewType.getState());
			}
		});

		/* Help */
		JMenuItem mnuHelpAbout = new JMenuItem("About");
		mnuHelpAbout.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent ae) {
				throw new UnsupportedOperationException("Not supported yet.");
			}
		});

		/* set menu together */
		mnu.add(mnuFile);
		mnuFile.add(new JSeparator());
		mnuFile.add(mnuFileExit);
		mnu.add(mnuView);
		mnuView.add(mnuViewLog);
		mnuViewLog.add(mnuViewVersion);
		mnuViewLog.add(mnuViewTimestamp);
		mnuViewLog.add(mnuViewType);
		mnu.add(mnuHelp);

		return mnu;
	}

	class BGPanel extends JPanel {
		private static final long serialVersionUID = -3274881478686424190L;
		// The Image to store the background image in.
		Image img;
		Color clr;

		public BGPanel(String path, Color defclr) {
			this.clr = defclr;
			// Loads the background image and stores in img object.
			this.img = Toolkit.getDefaultToolkit().createImage(path);
		}

		public void paint(Graphics g) {
			// Draws the img to the BackgroundPanel.
			g.setColor(clr);
			g.fillRect(0, 0, getWidth(), getHeight());
			g.drawImage(img, 0, 0, null);
		}
	}

	/**
	 * Initializes the content panel.
	 * @return A content panel instance
	 */
	private JPanel initContentPanel() {
		JPanel pnl = new JPanel();
		pnl.setLayout(new BorderLayout());

		pnlLeft = new JPanel(new GridLayout(3, 2));

		pnlCore = createGroupPanel("Core", pnlLeft);
		pnlEngine = createGroupPanel("Engine", pnlLeft);
		pnlSensor = createGroupPanel("Sensor", pnlLeft);
		pnlIO = createGroupPanel("I/O State", pnlLeft);
		pnlPeriphery = createGroupPanel("Periphery", pnlLeft);
		pnlComm = createGroupPanel("Communication", pnlLeft);

		pnlStats = new BGPanel("bg.jpg", Color.white);

		pnlLog = new JPanel(new BorderLayout());
		pnLog = new JTextPane();
		doc = pnLog.getStyledDocument();
		initStyles();

		spLog = new JScrollPane(pnLog);
		pnLog.setDoubleBuffered(true);
		pnLog.setEditable(false);

		spLog.setAutoscrolls(true);
		pnlLog.add(spLog);

		spRight = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, pnlStats, pnlLog);
		spMain = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true, pnlLeft, spRight);

		pnl.add(spMain, BorderLayout.CENTER);

		return pnl;
	}

	/**
	 * Create a scrollable group panel.
	 * @param label The text of the group box
	 * @param parent The parent (where to add)
	 * @return The scrollable group panel
	 */
	private JPanel createGroupPanel(final String label, final JPanel parent) {
		JPanel pnl = new JPanel(new GridLayout(0, 2));
		JScrollPane jsp = new JScrollPane(pnl);
		jsp.setBorder(BorderFactory.createTitledBorder(label));
		parent.add(jsp);
		return pnl;
	}

	/**
	 * Initializes the status panel.
	 * @return A status panel instance
	 */
	private JPanel initStatusPanel() {
		JPanel pnl = new JPanel();
		pnl.setLayout(new BoxLayout(pnl, BoxLayout.X_AXIS));
		pnl.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));

		lblMsgCount = createStatusItem("# Events", pnl);
		lblHostAddr = createStatusItem("Host = n/a:0", pnl);
		lblClientAddr = createStatusItem("Client = n/a", pnl);
		lblTime = createStatusItem("", pnl);

		return pnl;
	}

	/**
	 * Creates a status label.
	 * @param initText The initial text
	 * @param parent The parent panel to add
	 * @return The created label
	 */
	private JLabel createStatusItem(final String initText, final JPanel parent) {
		JLabel lbl = new JLabel(initText);
		lbl.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10));
		parent.add(lbl);
		parent.add(new JSeparator(JSeparator.VERTICAL));
		return lbl;
	}

	/**
	 * Initializes the styles for the rich text box
	 */
	private void initStyles() {
		def = createStyle(pnLog, "Default", "monospaced", false, Color.black);
		def_b = createStyle(pnLog, "DefaultBold", "monospaced", false, Color.black);
		def_i = createStyle(pnLog, "DefaultItalic", "monospaced", true, Color.black);

		err = createStyle(pnLog, "Err", "monospaced", false, Color.red);
		err_b = createStyle(pnLog, "ErrBold", "monospaced", false, Color.red);
		err_i = createStyle(pnLog, "ErrItalic", "monospaced", true, Color.red);

		warn = createStyle(pnLog, "Warn", "monospaced", false, COLORWARN);
		warn_b = createStyle(pnLog, "WarnBold", "monospaced", false, COLORWARN);
		warn_i = createStyle(pnLog, "WarnItalic", "monospaced", true, COLORWARN);

		inf = createStyle(pnLog, "Info", "monospaced", false, Color.blue);
		inf_b = createStyle(pnLog, "InfoBold", "monospaced", false, Color.blue);
		inf_i = createStyle(pnLog, "InfoItalic", "monospaced", true, Color.blue);
	}

	/**
	 * Create all the styles.
	 * @param parent Parent to attach to
	 * @param name The name of the style
	 * @param family The font family
	 * @param italic Should it be italic?
	 * @param color The color of the font
	 * @return A new style
	 */
	private Style createStyle(JTextPane parent, String name, String family,
	    boolean italic, Color color) {
		Style s = parent.addStyle(name, null);
		StyleConstants.setFontFamily(s, family);
		StyleConstants.setForeground(s, color);
		StyleConstants.setItalic(s, italic);
		return s;
	}

	/**
	 * @param args the command line arguments
	 */
	public static void main(String args[]) {
		/* Set the Nimbus look and feel */
		try {
			boolean set = false;
			for(javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager
			    .getInstalledLookAndFeels()) {
				if("Nimbus".equals(info.getName())) {
					javax.swing.UIManager.setLookAndFeel(info.getClassName());
					set = true;
					break;
				}
			}
			if(!set) {
				javax.swing.UIManager.setLookAndFeel(javax.swing.UIManager
				    .getSystemLookAndFeelClassName());
			}
		}
		catch(ClassNotFoundException ex) {
			java.util.logging.Logger.getLogger(AreionObservator.class.getName()).log(
			    java.util.logging.Level.SEVERE, null, ex);
		}
		catch(InstantiationException ex) {
			java.util.logging.Logger.getLogger(AreionObservator.class.getName()).log(
			    java.util.logging.Level.SEVERE, null, ex);
		}
		catch(IllegalAccessException ex) {
			java.util.logging.Logger.getLogger(AreionObservator.class.getName()).log(
			    java.util.logging.Level.SEVERE, null, ex);
		}
		catch(javax.swing.UnsupportedLookAndFeelException ex) {
			java.util.logging.Logger.getLogger(AreionObservator.class.getName()).log(
			    java.util.logging.Level.SEVERE, null, ex);
		}

		// Start new gui
		java.awt.EventQueue.invokeLater(new Runnable() {
			@Override
			public void run() {
				new AreionObservator().setVisible(true);
			}
		});
	}

	/* Server Listener */
	@Override
	public void messageReceived(final Message msg) {
		this.addMessage(msg);
	}

	@Override
	public void clientConnected(final String client) {
		lblClientAddr.setText("Last client = " + client);
		lblClientAddr.setForeground(new Color(32, 128, 0));
		setField(Module.COMMUNICATION, ch.areion.common.message.Type.INFO,
		    "Clients:" + ClientConnection.ClientCount);
	}

	@Override
	public void clientDisconnected() {
		ch.areion.common.message.Type tp = ch.areion.common.message.Type.INFO;

		if(ClientConnection.ClientCount == 0) {
			lblClientAddr.setText("Client = n/a");
			lblClientAddr.setForeground(Color.red);
			tp = ch.areion.common.message.Type.ERROR;
		}
		setField(Module.COMMUNICATION, tp, "Clients:"
		    + ClientConnection.ClientCount);
	}

	/* Buffer Methods */
	public void addMessage(final Message msg) {
		msgBuffer.add(msg);
	}

	/**
	 * Set or adds a field to the gui.
	 * @param m The Module (where to add)
	 * @param t The color of the font.
	 * @param msgData The field and data to set.
	 */
	private void setField(Module m, ch.areion.common.message.Type t,
	    String msgData) {
		HashMap<String, InfoItem> map = null;
		JPanel pnlGrp = null;
		switch(m) {
			case CORE:
				map = coreItems;
				pnlGrp = pnlCore;
				break;
			case ENGINE:
				map = engineItems;
				pnlGrp = pnlEngine;
				break;
			case SENSOR:
				map = sensorItems;
				pnlGrp = pnlSensor;
				break;
			case IO:
				map = ioItems;
				pnlGrp = pnlIO;
				break;
			case PERIPHERY:
				map = peripheryItems;
				pnlGrp = pnlPeriphery;
				break;
			case COMMUNICATION:
				map = commItems;
				pnlGrp = pnlComm;
				break;
			default:
				map = null;
				pnlGrp = null;
		}

		if(map != null) {
			StringTokenizer tkn = new StringTokenizer(msgData, ":");
			String id = tkn.nextToken();
			String data = tkn.nextToken();
			while(tkn.hasMoreElements())
				data += ":" + tkn.nextToken();

			InfoItem ii;
			if(!map.containsKey(id)) {
				ii = new InfoItem(id, data);
				map.put(id, ii);
				pnlGrp.add(ii.getLabel());
				pnlGrp.add(ii.getTextField());
			}
			else {
				ii = map.get(id);
				ii.getTextField().setText(data);
			}
			setTextFieldColor(ii, t);
		}
	}

	/**
	 * Set or add a field to the gui.
	 * @param msg The message which content the correct information
	 */
	private void setField(Message msg) {
		setField(msg.getModule(), msg.getType(), msg.getData());
	}

	/**
	 * Sets the foreground color of a given info item.
	 * @param ii The info item to set
	 * @param tp The colortype to set
	 */
	private void setTextFieldColor(InfoItem ii, ch.areion.common.message.Type tp) {
		Color c;
		switch(tp) {
			case INFO:
			default:
				c = Color.black;
				break;
			case WARNING:
				c = COLORWARN;
				break;
			case ERROR:
				c = Color.RED;
				break;
		}
		ii.getTextField().setForeground(c);
	}

	/**
	 * Clears the message buffer.
	 */
	private void clearBuffer() {
		Message msg;

		if(doc.getLength() > 100000) {
			pnLog.setText("");
		}

		while(msgBuffer.size() > 0) {
			msg = msgBuffer.remove(0);

			try {
				ch.areion.common.message.Type tp = msg.getType();

				// add to gui
				if(msg.getData().contains(":"))
					setField(msg);

				// version
				if(mnuViewVersion.getState()) {
					doc.insertString(doc.getLength(),
					    msg.getVersion().name().substring(0, 3) + " ", def);
				}

				// timestamp
				if(mnuViewTimestamp.getState()) {
					doc.insertString(doc.getLength(),
					    String.format("%-24s", msg.getTimestamp().toString()), def);
				}

				// type
				if(mnuViewType.getState()) {
					doc.insertString(doc.getLength(), "[" + tp.name().substring(0, 1)
					    + "] ", getStyle(tp, 1));
				}

				// module
				doc.insertString(doc.getLength(), "{" + msg.getModule().name() + "} ",
				    getStyle(tp, 0));

				// data
				doc.insertString(doc.getLength(), "" + msg.getData(), getStyle(tp, 2));

				// new line
				doc.insertString(doc.getLength(), "\n", def);
			}
			catch(BadLocationException e) {
				System.err.println("Unable to write message \"" + msg.format() + "\":"
				    + e);
			}
		}

		lblMsgCount.setText(Message.Count + " Events");
	}

	/**
	 * Returns the correct style to the message.
	 * @param t The type of the message
	 * @param s The style type (Default/Bold/Italic)
	 * @return The correct style
	 */
	private Style getStyle(ch.areion.common.message.Type t, int s) {
		switch(t) {
			case INFO:
				switch(s) {
					case 1:
						return inf_b;
					case 2:
						return inf_i;
				}
				return inf;
			case WARNING:
				switch(s) {
					case 1:
						return warn_b;
					case 2:
						return warn_i;
				}
				return warn;
			case ERROR:
				switch(s) {
					case 1:
						return err_b;
					case 2:
						return err_i;
				}
				return err;
		}

		switch(s) {
			case 1:
				return def_b;
			case 2:
				return def_i;
		}
		return def;
	}

}